'use strict';

var bind = require('./helpers/bind');

// utils是一个非特定于axios的通用辅助函数库

var toString = Object.prototype.toString;

/**
 * 确定值是否为数组
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值是数组，则为True，否则为false
 */
function isArray(val) {
  return toString.call(val) === '[object Array]';
}

/**
 * 确定值是否为undefined
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值是 undefined，则为True，否则为false
 */
function isUndefined(val) {
  return typeof val === 'undefined';
}

/**
 * 确定值是否为 Buffer
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值是 Buffer，则为True，否则为false
 */
function isBuffer(val) {
  return (
    val !== null &&
    !isUndefined(val) &&
    val.constructor !== null &&
    !isUndefined(val.constructor) &&
    typeof val.constructor.isBuffer === 'function' &&
    val.constructor.isBuffer(val)
  );
}

/**
 * 确定值是否为 ArrayBuffer
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 ArrayBuffer，则为True，否则为false
 */
function isArrayBuffer(val) {
  return toString.call(val) === '[object ArrayBuffer]';
}

/**
 * 确定值是否为 FormData
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 FormData，则为True，否则为false
 */
function isFormData(val) {
  return typeof FormData !== 'undefined' && val instanceof FormData;
}

/**
 * 确定值是否为ArrayBuffer上的视图
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 ArrayBuffer上的视图，则为True，否则为false
 */
function isArrayBufferView(val) {
  var result;
  if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {
    result = ArrayBuffer.isView(val);
  } else {
    result = val && val.buffer && val.buffer instanceof ArrayBuffer;
  }
  return result;
}

/**
 * 确定值是否为字符串
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 字符串，则为True，否则为false
 */
function isString(val) {
  return typeof val === 'string';
}

/**
 * 确定值是否为数字
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 数字，则为True，否则为false
 */
function isNumber(val) {
  return typeof val === 'number';
}

/**
 * 确定值是否为 对象
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 对象，则为True，否则为false
 */
function isObject(val) {
  return val !== null && typeof val === 'object';
}

/**
 * 确定值是否为 普通对象
 *
 * @param {Object} val 要测试的值
 * @return {boolean} 如果值为 普通对象，则为True，否则为false
 */
function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false;
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

/**
 * 确定值是否为 日期Date
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 Date，则为True，否则为false
 */
function isDate(val) {
  return toString.call(val) === '[object Date]';
}

/**
 * 确定值是否为 文件File
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 File，则为True，否则为false
 */
function isFile(val) {
  return toString.call(val) === '[object File]';
}

/**
 * 确定值是否为 Blob
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 Blob，则为True，否则为false
 */
function isBlob(val) {
  return toString.call(val) === '[object Blob]';
}

/**
 * 确定值是否为 函数Function
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 Function，则为True，否则为false
 */
function isFunction(val) {
  return toString.call(val) === '[object Function]';
}

/**
 * 确定值是否为 流Stream
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 Stream，则为True，否则为false
 */
function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}

/**
 * 确定值是否为 URLSearchParams对象
 *
 * @param {Object} val 要测试的值
 * @returns {boolean} 如果值为 URLSearchParams对象，则为True，否则为false
 */
function isURLSearchParams(val) {
  return (
    typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
  );
}

/**
 * 修剪字符串开头和结尾多余的空白
 *
 * @param {String} str 要修剪的字符串
 * @returns {String} 待修改的字符串
 */
function trim(str) {
  return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
}

/**
 * 确定我们是否在标准浏览器环境中运行
 *
 * 这允许axios在web worker中运行，并以本机方式进行响应。
 * 这两种环境都支持XMLHttpRequest，但不完全支持标准的globals。
 *
 * web workers:
 *  typeof window -> undefined
 *  typeof document -> undefined
 *
 * react-native:
 *  navigator.product -> 'ReactNative'
 * nativescript
 *  navigator.product -> 'NativeScript' or 'NS'
 *
 * navigator.product弃用说明
 *  https://developer.mozilla.org/en-US/docs/Web/API/Navigator/product
 */
function isStandardBrowserEnv() {
  if (
    typeof navigator !== 'undefined' &&
    (navigator.product === 'ReactNative' ||
      navigator.product === 'NativeScript' ||
      navigator.product === 'NS')
  ) {
    return false;
  }
  return typeof window !== 'undefined' && typeof document !== 'undefined';
}

/**
 * 迭代调用每个项的函数的数组或对象。
 *
 * 如果"obj"是数组，将调用回调函数，传递每个项的值、索引和完整数组。
 *
 * 如果"obj"是一个对象，则将调用回调，为每个属性传递值、键和完整对象。
 *
 * @param {Object|Array} obj 要迭代的对象
 * @param {Function} fn 要为每个项调用的回调
 */
function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // 强制数组（如果还不是可测试的）
  if (typeof obj !== 'object') {
    /* eslint no-param-reassign:0 */
    obj = [obj];
  }

  if (isArray(obj)) {
    // 迭代数组值
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 迭代对象键
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

/**
 * 接受varargs，期望每个参数都是一个对象，然后不变地合并每个对象的属性并返回结果。
 *
 * 当多个对象包含相同的键时，参数列表中较晚的对象将被优先考虑。
 * 
 * 后面的覆盖前面的
 *
 * 例子:
 *
 * ```js
 * let result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // outputs 456
 * ```
 *
 * @param {Object} obj1 要合并的对象
 * @returns {Object} 所有合并属性的结果
 */
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      // 如果是已合并结果的该key的value是普通对象并且要合并的值也是普通对象
      // 递归合并
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      // 已合并结果中该key不是普通对象，并且要合并的值是普通对象
      // 直接覆盖
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      // 已合并结果中该key不是普通对象，并且要合并的值是数组
      // 直接用新数组覆盖
      result[key] = val.slice();
    } else {
      // 否则直接覆盖
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    // 遍历对象
    forEach(arguments[i], assignValue);
  }
  return result;
}

/**
 * 通过向对象a添加对象b的属性来扩展对象a。
 *
 * @param {Object} a 要扩展的对象
 * @param {Object} b 要复制属性的对象
 * @param {Object} thisArg 绑定函数的对象
 * @return {Object} 对象a的结果值
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

/**
 * 移除字节顺序标记。这就抓住了EF BB BF（UTF-8的BOM）。
 *
 * @param {string} content BOM 内容
 * @return {string} 不含BOM的内容值
 */
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xfeff) {
    content = content.slice(1);
  }
  return content;
}

module.exports = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject,
  isPlainObject: isPlainObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  extend: extend,
  trim: trim,
  stripBOM: stripBOM,
};
